﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
using StyleMVVM.Data;
#if !DOT_NET
using Windows.Foundation;
using Windows.Storage;
using Windows.Storage.Streams;
#endif
using StyleMVVM.DependencyInjection;
#if WINDOWS_PHONE
using StyleMVVM.Data.Collections;
#else
using System.Collections.Concurrent;
#endif
#if NETFX_CORE
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
#else
using System.Windows;
using System.Windows.Controls;

#endif

namespace StyleMVVM.Suspension
{
	public sealed class FrameSuspensionManager : IFrameSuspensionService
	{
		private static readonly object lockObject = new object();
		private static volatile IFrameSuspensionService instance;
		private Dictionary<string, object> _sessionState = new Dictionary<string, object>();
		private const string sessionStateFilename = "_sessionState.xml";

		public static void RegisterExport(IDependencyInjectionContainer container)
		{
			container.Register(typeof(FrameSuspensionManager)).
			          As(typeof(IFrameSuspensionService)).AndSharedPermenantly();
		}

		public static IFrameSuspensionService Instance
		{
			get
			{
				if (instance == null)
				{
					lock (lockObject)
					{
						if (instance == null)
						{
							instance =
								Bootstrapper.Instance.Container.LocateByType(
									typeof(IFrameSuspensionService)) as IFrameSuspensionService;
						}
					}
				}

				return instance;
			}
		}

		/// <summary>
		/// Provides access to global session state for the current session.  This state is
		/// serialized by <see cref="SuspendAsync"/> and restored by
		/// <see cref="RestoreAsync"/>, so values must be serializable by
		/// <see cref="DataContractSerializer"/> and should be as compact as possible.  Strings
		/// and other self-contained data types are strongly recommended.
		/// </summary>
		public IDictionary<string, object> SessionState
		{
			get { return _sessionState; }
		}

		/// <summary>
		/// List of custom types provided to the <see cref="DataContractSerializer"/> when
		/// reading and writing session state.  Initially empty, additional types may be
		/// added to customize the serialization process.
		/// </summary>
		public IReadOnlyList<Type> KnownTypes
		{
			get { return ReflectionService.KnownTypes; }
		}

#if NETFX_CORE
		public IAsyncAction RestoreAsync()
		{
			return InternalRestoreAsync().AsAsyncAction();
		}

		public IAsyncAction SaveAsync()
		{
			return InternalSaveAsync().AsAsyncAction();
		}
#else
		public Task RestoreAsync()
		{
			return InternalRestoreAsync();
		}

		public Task SaveAsync()
		{
			return InternalSaveAsync();
		}
#endif

		/// <summary>
		/// Save the current <see cref="SessionState"/>.  Any <see cref="Frame"/> instances
		/// registered with <see cref="RegisterFrame"/> will also preserve their current
		/// navigation stack, which in turn gives their active <see cref="Page"/> an opportunity
		/// to save its state.
		/// </summary>
		/// <returns>An asynchronous task that reflects when session state has been saved.</returns>
		internal async Task InternalSaveAsync()
		{
			// Save the navigation state for all registered frames
			foreach (var weakFrameReference in _registeredFrames)
			{
				Frame frame;

				if (weakFrameReference.TryGetTarget(out frame))
				{
					SaveFrameNavigationState(frame);
				}
			}

			// Serialize the session state synchronously to avoid asynchronous access to shared
			// state
			MemoryStream sessionData = new MemoryStream();
			DataContractSerializer serializer = new DataContractSerializer(typeof(Dictionary<string, object>), KnownTypes);
			serializer.WriteObject(sessionData, _sessionState);

#if DOT_NET
			// TODO
#else
	// Get an output stream for the SessionState file and write the state asynchronously
			StorageFile file =
				await
				ApplicationData.Current.LocalFolder.CreateFileAsync(sessionStateFilename, CreationCollisionOption.ReplaceExisting);
			using (Stream fileStream = await file.OpenStreamForWriteAsync())
			{
				sessionData.Seek(0, SeekOrigin.Begin);
				await sessionData.CopyToAsync(fileStream);
				await fileStream.FlushAsync();
			}
#endif
		}

		/// <summary>
		/// Restores previously saved <see cref="SessionState"/>.  Any <see cref="Frame"/> instances
		/// registered with <see cref="RegisterFrame"/> will also restore their prior navigation
		/// state, which in turn gives their active <see cref="Page"/> an opportunity restore its
		/// state.
		/// </summary>
		/// <returns>An asynchronous task that reflects when session state has been read.  The
		/// content of <see cref="SessionState"/> should not be relied upon until this task
		/// completes.</returns>
		internal async Task InternalRestoreAsync()
		{
			_sessionState = new Dictionary<String, Object>();

#if DOT_NET

#else
	// Get the input stream for the SessionState file
			StorageFile file = await ApplicationData.Current.LocalFolder.GetFileAsync(sessionStateFilename);
			using (IInputStream inStream = await file.OpenSequentialReadAsync())
			{
				// Deserialize the Session State
				DataContractSerializer serializer = new DataContractSerializer(typeof(Dictionary<string, object>), KnownTypes);
				_sessionState = (Dictionary<string, object>)serializer.ReadObject(inStream.AsStreamForRead());
			}

			// Restore any registered frames to their saved state
			foreach (var weakFrameReference in _registeredFrames)
			{
				Frame frame;
				if (weakFrameReference.TryGetTarget(out frame))
				{
					frame.ClearValue(FrameSessionStateProperty);
					RestoreFrameNavigationState(frame);
				}
			}
#endif
		}

		private static readonly DependencyProperty FrameSessionStateKeyProperty =
			DependencyProperty.RegisterAttached("_FrameSessionStateKey", typeof(String), typeof(FrameSuspensionManager), null);

		private static readonly DependencyProperty FrameSessionStateProperty =
			DependencyProperty.RegisterAttached("_FrameSessionState",
			                                    typeof(Dictionary<String, Object>),
			                                    typeof(FrameSuspensionManager),
			                                    null);

		private static readonly List<WeakReference<Frame>> _registeredFrames = new List<WeakReference<Frame>>();

		/// <summary>
		/// Registers a <see cref="Frame"/> instance to allow its navigation history to be saved to
		/// and restored from <see cref="SessionState"/>.  Frames should be registered once
		/// immediately after creation if they will participate in session state management.  Upon
		/// registration if state has already been restored for the specified key
		/// the navigation history will immediately be restored.  Subsequent invocations of
		/// <see cref="RestoreAsync"/> will also restore navigation history.
		/// </summary>
		/// <param name="frame">An instance whose navigation history should be managed by
		/// <see cref="SuspensionManager"/></param>
		/// <param name="sessionStateKey">A unique key into <see cref="SessionState"/> used to
		/// store navigation-related information.</param>
		public void RegisterFrame(Frame frame, String sessionStateKey)
		{
			if (frame.GetValue(FrameSessionStateKeyProperty) != null)
			{
				throw new InvalidOperationException("Frames can only be registered to one session state key");
			}

			if (frame.GetValue(FrameSessionStateProperty) != null)
			{
				throw new InvalidOperationException(
					"Frames must be either be registered before accessing frame session state, or not registered at all");
			}

			// Use a dependency property to associate the session key with a frame, and keep a list of frames whose
			// navigation state should be managed
			frame.SetValue(FrameSessionStateKeyProperty, sessionStateKey);
			_registeredFrames.Add(new WeakReference<Frame>(frame));

			// Check to see if navigation state can be restored
			RestoreFrameNavigationState(frame);
		}

		/// <summary>
		/// Disassociates a <see cref="Frame"/> previously registered by <see cref="RegisterFrame"/>
		/// from <see cref="SessionState"/>.  Any navigation state previously captured will be
		/// removed.
		/// </summary>
		/// <param name="frame">An instance whose navigation history should no longer be
		/// managed.</param>
		public void UnregisterFrame(Frame frame)
		{
			// Remove session state and remove the frame from the list of frames whose navigation
			// state will be saved (along with any weak references that are no longer reachable)
			SessionState.Remove((String)frame.GetValue(FrameSessionStateKeyProperty));
			_registeredFrames.RemoveAll((weakFrameReference) =>
				                            {
					                            Frame testFrame;
					                            return !weakFrameReference.TryGetTarget(out testFrame) || testFrame == frame;
				                            });
		}

		/// <summary>
		/// Provides storage for session state associated with the specified <see cref="Frame"/>.
		/// Frames that have been previously registered with <see cref="RegisterFrame"/> have
		/// their session state saved and restored automatically as a part of the global
		/// <see cref="SessionState"/>.  Frames that are not registered have transient state
		/// that can still be useful when restoring pages that have been discarded from the
		/// navigation cache.
		/// </summary>
		/// <remarks>Apps may choose to rely on <see cref="LayoutAwarePage"/> to manage
		/// page-specific state instead of working with frame session state directly.</remarks>
		/// <param name="frame">The instance for which session state is desired.</param>
		/// <returns>A collection of state subject to the same serialization mechanism as
		/// <see cref="SessionState"/>.</returns>
		public IDictionary<String, Object> SessionStateForFrame(Frame frame)
		{
			var frameState = (Dictionary<String, Object>)frame.GetValue(FrameSessionStateProperty);

			if (frameState == null)
			{
				var frameSessionKey = (String)frame.GetValue(FrameSessionStateKeyProperty);
				if (frameSessionKey != null)
				{
					// Registered frames reflect the corresponding session state
					if (!_sessionState.ContainsKey(frameSessionKey))
					{
						_sessionState[frameSessionKey] = new Dictionary<String, Object>();
					}
					frameState = (Dictionary<String, Object>)_sessionState[frameSessionKey];
				}
				else
				{
					// Frames that aren't registered have transient state
					frameState = new Dictionary<String, Object>();
				}
				frame.SetValue(FrameSessionStateProperty, frameState);
			}
			return frameState;
		}

		private void RestoreFrameNavigationState(Frame frame)
		{
			var frameState = SessionStateForFrame(frame);
			if (frameState.ContainsKey("Navigation"))
			{
#if NETFX_CORE
				frame.SetNavigationState((String)frameState["Navigation"]);
#endif
			}
		}

		private void SaveFrameNavigationState(Frame frame)
		{
#if NETFX_CORE
			var frameState = SessionStateForFrame(frame);
			frameState["Navigation"] = frame.GetNavigationState();
#endif
		}
	}
}